16|渲染缩放

loading

16.1 可变分辨率

应用程序一般都以固定分辨率运行,某些应用会允许用户通过设置菜单更改分辨率,但这需要完全重新初始化图形。更好的方法是保持应用程序的分辨率固定,改变相机用于渲染的缓冲区大小。这会影响整个渲染过程,除了最终绘制到帧缓冲区,此时结果将重新缩放以匹配应用的分辨率。

可以通过缩放缓冲区大小以减少要处理的片元数量来提高性能。例如可以针对所有3D渲染进行此操作,同时保持UI在全分辨率下清晰,也可以动态调整缩放比例,以保持帧速率可接受。最后还可以把当前缓冲区图像分辨率成倍提高,进行采样和混合后将最终图像输出到最终显示屏幕中,从而减少有限分辨率引起的锯齿伪影,这种方法称为SSAA(超采样抗锯齿)。

16.1.1 缓冲区设置

1. 调整渲染缩放会影响缓冲区大小,因此我们在CameraBufferSettings脚本中添加一个可以调整渲染缩放比例的滑块,区间为[0.1,2]。如果使用单个双线性插值步骤重新缩放比例,那么高于2不会提高图形质量,相反会降低质量。因为我们在下采样到最终目标分辨率时会跳过许多像素。

    [Range(0.1f, 2f)]
public float renderScale;

2. 在CustomRenderPipelineAsset中创建CameraBufferSettings对象时设置默认缩放比例为1。

    CameraBufferSettings cameraBuffer = new CameraBufferSettings
{
allowHDR = true,
renderScale = 1f
};

loading

3. 然后在CameraRenderer脚本中添加一个bool字段用于追踪是否使用渲染缩放。

bool useScaledRendering;

4. 我们不想让渲染缩放影响Scene视图,因为这是用来编辑场景的,所以在PrepareForSceneWindow方法中禁用渲染缩放。

    partial void PrepareForSceneWindow()
{
if (camera.cameraType == CameraType.SceneView)
{
ScriptableRenderContext.EmitWorldGeometryForSceneView(camera);
//禁用渲染缩放
useScaledRendering = false;
}
}

5. 在Render方法调用PrepareForSceneWindow方法之前判断是否使用渲染缩放。以1为基准,如果差异在1%以外则启用。

        float renderScale = bufferSettings.renderScale;
useScaledRendering = renderScale < 0.99f || renderScale > 1.01f;
PrepareBuffer();
PrepareForSceneWindow();

6. 调整Setup方法使用中间帧缓冲区的检查条件,使用渲染缩放时也需要使用中间帧缓冲区。

useIntermediateBuffer = useScaledRendering || useColorTexture || useDepthTexture || postFXStack.IsActive;

16.1.2 缓冲区大小

1. 因为我们的相机缓冲区大小现在可能和Camera组件表示的缓冲区大小不同,所以我们需要跟踪最终使用的缓冲区大小,在CameraRenderer脚本中定义一个Vector2Int类型的字段。

    //最终使用的缓冲区大小
Vector2Int bufferSize;

2. 在Render方法中剔除操作之后,设置合适的缓冲区大小,如果是按渲染比例进行缩放,要将结果转换为整数值。

       if (!Cull(shadowSettings.maxDistance))
{
return;
}
useHDR = bufferSettings.allowHDR && camera.allowHDR;
//按比例缩放相机屏幕像素尺寸
if (useScaledRendering)
{
bufferSize.x = (int)(camera.pixelWidth * renderScale);
bufferSize.y = (int)(camera.pixelHeight * renderScale);
}
else
{
bufferSize.x = camera.pixelWidth;
bufferSize.y = camera.pixelHeight;
}

3. 在Setup方法中获取相机颜色和深度附件的渲染纹理时,使用最终的缓冲区大小,包括在CopyAttachments方法中获取颜色和深度纹理时也进行调整。

    void Setup()
{
...
if (useIntermediateBuffer)
{
if (flags > CameraClearFlags.Color)
{
flags = CameraClearFlags.Color;
}
buffer.GetTemporaryRT(colorAttachmentId, bufferSize.x, bufferSize.y, 0, FilterMode.Bilinear,
useHDR ? RenderTextureFormat.DefaultHDR : RenderTextureFormat.Default);
buffer.GetTemporaryRT(depthAttachmentId, bufferSize.x, bufferSize.y, 32, FilterMode.Point, RenderTextureFormat.Depth);
...
}
...

}
void CopyAttachments()
{
if (useColorTexture)
{
buffer.GetTemporaryRT(colorTextureId, bufferSize.x, bufferSize.y, 0, FilterMode.Bilinear,
useHDR ? RenderTextureFormat.DefaultHDR : RenderTextureFormat.Default);
...
}
if (useDepthTexture)
{
buffer.GetTemporaryRT(depthTextureId, bufferSize.x, bufferSize.y, 32, FilterMode.Point, RenderTextureFormat.Depth);
...
}
...
}

下图是没有启用后处理的情况下将缓冲区缩放比例调节成0.3和2的对比。小的缩放比例可以使渲染速度加快,但是图像质量会降低,图像会糊掉,大的缩放比例则相反。当不启用后处理时,调整后的缩放比例需要一个中间帧缓冲区和一些额外的绘制,因此增加了一些额外工作。

loading

loading

16.1.3 片元屏幕UV

调整渲染缩放会引入一个错误,采样颜色和深度纹理时会出错。如下图所示,我们将渲染缩放比例设置为1.5时,粒子扰动不太正常,这是由于使用了不正确的屏幕空间UV坐标造成的。

loading

loading

1. 因为Unity的_ScreenParams数据和相机的像素尺寸匹配,而不是和我们的目标缓冲区尺寸匹配,所以我们通过使用_CameraBufferSize向量来解决这个问题,该向量包含相机调整后的尺寸数据。首先声明一个相机缓冲区大小着色器标识ID。

 static int bufferSizeId = Shader.PropertyToID("_CameraBufferSize");

2. 在Render方法中确定最终缓冲区大小后,我们将尺寸数据存储到向量中并发送到GPU。

        if (useScaledRendering)
{
bufferSize.x = (int)(camera.pixelWidth * renderScale);
bufferSize.y = (int)(camera.pixelHeight * renderScale);
}
else
{
bufferSize.x = camera.pixelWidth;
bufferSize.y = camera.pixelHeight;
}
buffer.BeginSample(SampleName);
buffer.SetGlobalVector(bufferSizeId, new Vector4(1f / bufferSize.x, 1f / bufferSize.y,bufferSize.x, bufferSize.y));
ExecuteBuffer();

3. 在Fragment文件中声明该向量,并使用它的XY分量替换_ScreenParams.xy,我们在CPU中已经完成了除法计算。

float4 _CameraBufferSize;
Fragment GetFragment (float4 positionSS)
{
Fragment f;
f.positionSS = positionSS.xy;
f.screenUV = f.positionSS * _CameraBufferSize.xy; // / _ScreenParams.xy;
...
}

现在得到了正确的粒子扰动效果。

loading

16.1.4 缩放后处理

1. 调整渲染缩放比例也会影响后处理,否则最终会意外缩放。最好的办法是始终使用相同的缓冲区大小,因此我们将其通过CameraRenderer.Render方法调用postFXStack.Setup时作为第三个参数传入。

postFXStack.Setup(context, camera, bufferSize,postFXSettings, useHDR, colorLUTResolution,cameraSettings.finalBlendMode);

2. 然后在PostFXStack脚本中追踪该属性。

    Vector2Int bufferSize;
public void Setup(ScriptableRenderContext context, Camera camera, Vector2Int bufferSize, ...)
{
this.bufferSize = bufferSize;
...
}

3. 调整DoBloom方法,将相机屏幕尺寸改为缓冲区大小进行计算。

bool DoBloom(int sourceId)
{
PostFXSettings.BloomSettings bloom = settings.Bloom;
int width = bufferSize.x / 2, height = bufferSize.y / 2;
...
buffer.GetTemporaryRT(bloomPrefilterId, bufferSize.x, bufferSize.y, 0, FilterMode.Bilinear, format);
...
}

Bloom是一种依赖分辨率的效果,所以调整渲染缩放比例会改变它的外观,迭代几次Bloom比较容易观察,减小缩放比例会使Bloom效果变大,而增大则使得Bloom效果变小,具有最大迭代次数的Bloom变化不大,但由于分辨率的变化,调整缩放比例时可能出现跳动。下面是2次Bloom迭代时渲染缩放比例为0.5和2时的对比图。

loading

loading

4. 特别是逐渐调整渲染缩放比例,最好尽可能保持Bloom效果一致,可以通过将Bloom金字塔的起始尺寸建立在相机上而不是缓冲区大小来做到这一点。我们在BloomSettings结构体中定义一个bool字段作为切换开关来决定是否忽略渲染缩放。

public struct BloomSettings
{
...
//是否忽略渲染缩放
public bool ignoreRenderScale;
}

5. 在PostFXStack的DoBloom方法中进行调整,如果忽略渲染缩放,则像以前一样获取渲染纹理时使用摄像机屏幕像素一半的尺寸大小,这意味着它不再执行默认下采样至一半的分辨率,而取决于渲染缩放比例。最终的Bloom效果应和缩放后的缓冲区大小相匹配,以便在末尾引入另一个自动向下或向上采样的步骤。

bool DoBloom(int sourceId)
{
PostFXSettings.BloomSettings bloom = settings.Bloom;
int width, height;
if (bloom.ignoreRenderScale)
{
width = camera.pixelWidth / 2;
height = camera.pixelHeight / 2;
}
else
{
width = bufferSize.x / 2;
height = bufferSize.y / 2;
}
...
}

loading

loading

loading

16.1.5 逐相机的渲染缩放

接下来我们支持每个相机可以使用不同的渲染缩放比例,它可以覆盖掉渲染管线的全局渲染缩放比例,也可以继续沿用全局缩放比例。

1. 我们在CameraSettings脚本中添加一个调整渲染缩放比例的滑块,并且定义一个RenderScaleMode枚举,可以将渲染缩放模式设置为继承,叠加相乘或者覆盖。最后我们定义一个GetRenderScale方法根据当前设置的模式获得最终的渲染缩放比例。

    public enum RenderScaleMode
{
Inherit,
Multiply,
Override
}
public RenderScaleMode renderScaleMode = RenderScaleMode.Inherit;
[Range(0.1f, 2f)]
public float renderScale = 1f;
public float GetRenderScale(float scale)
{
return renderScaleMode == RenderScaleMode.Inherit ? scale :
renderScaleMode == RenderScaleMode.Override ? renderScale : scale * renderScale;
}

loading

2. 调整CameraRenderer的Render方法中获取最终渲染缩放比例的方法。我们还可以对该值进行Clamp限制,将缩放比例限制在[0.1,2]范围区间,防止其过小或过大。

    public void Render(...)
{
...
float renderScale = cameraSettings.GetRenderScale(bufferSettings.renderScale);
...
if (useScaledRendering)
{
renderScale = Mathf.Clamp(renderScale, 0.1f, 2f);
bufferSize.x = (int)(camera.pixelWidth * renderScale);
bufferSize.y = (int)(camera.pixelHeight * renderScale);
}
...
}

下图是两个相机使用了不同的渲染缩放比例的效果。

loading


16.2 重新缩放

当使用除1以外的缩放比例时,除了最终绘制到相机的目标缓冲区外,所有内容都应该以该缩放后的比例进行。如果没有使用后处理,这只是一个简单的拷贝,重新缩放比例到最终大小。但如果启用了后处理,它就成了最终绘制,也隐性地执行了重新缩放,然而最终绘制时重新缩放会带来一些不利因素。

16.2.1 当前方法

我们当前的重新缩放方法会产生一些副作用,比如在向上或向下缩放超过1的HDR颜色时总是有锯齿,插值只有在LDR中执行才有平滑的效果,HDR的插值结果仍然大于1,这些结果根本不会显示混合,例如0和10的平均值是5。在LDR中0和1的平均值似乎是1,而我们本来预计它是0.5。

下图是开启HDR时的缩放比例为0.5和2的效果。

loading

loading

下图是关闭HDR时的缩放比例为0.5和2的效果。

loading

loading

在Final Pass期间重新缩放的第二个问题是,颜色校正应用于插值颜色而不是原始颜色,这可能会引入不希望出现的色带。最明显的是阴影和高光直接插值时会出现中间色调(Midtones),可以通过将中间色调调整为比较强烈的颜色值(比如红色)进行观察,下面是缩放比例为0.5和2时的效果。

loading

loading

16.2.2 重新缩放LDR

1. 锐利的HDR边缘和颜色校正伪影都是因为颜色校正和色调映射之前对HDR颜色插值引起的,所以解决方法是同时调整两者的渲染缩放比例,然后执行另一个Copy Pass重新缩放LDR颜色。在PostFXStack.shader中添加一个Final Rescale Pass来处理最后一步。它是一个Copy Pass,也有可配置的混合模式,记得也在Pass枚举中定义它。

Pass 
{
Name "Final Rescale"
Blend [_FinalSrcBlend] [_FinalDstBlend]
HLSLPROGRAM
#pragma target 3.5
#pragma vertex DefaultPassVertex
#pragma fragment CopyPassFragment
ENDHLSL
}

enum Pass
{
...
Final,
FinalRescale
}

2. 现在我们有两个Final Pass,给DrawFinal方法添加一个Pass枚举参数。

void DrawFinal(RenderTargetIdentifier from, Pass pass)
{
...
buffer.DrawProcedural(Matrix4x4.identity, settings.Material, (int)pass, MeshTopology.Triangles, 3);
}

3. 现在在DoColorGradingAndToneMapping中使用哪种方法取决于我们是否正在使用调整后的渲染缩放比例,我们可以通过比较缓冲区大小和像素大小来检验这一点,一般检查宽度就可以了。如果相等,则还像以前一样绘制Fianl Pass,但如果需要重新缩放,我们需要绘制两次。首先获得一个新的临时渲染纹理并匹配当前缓冲区的大小,当我们在其中存储LDR颜色时可以使用默认渲染纹理格式,然后在Final Pass进行常规绘制,并将混合模式设置为One Zero。然后再进行最后的Rescale Pass,并释放中间缓冲区。

int finalResultId = Shader.PropertyToID("_FinalResultId");
void DoColorGradingAndToneMapping(int sourceId)
{
...
if (bufferSize.x == camera.pixelWidth)
{
DrawFinal(sourceId, Pass.Final);
}
else
{
buffer.SetGlobalFloat(finalSrcBlendId, 1f);
buffer.SetGlobalFloat(finalDstBlendId, 0f);
buffer.GetTemporaryRT(finalResultId, bufferSize.x, bufferSize.y, 0,FilterMode.Bilinear, RenderTextureFormat.Default);
Draw(sourceId, finalResultId, Pass.Final);
DrawFinal(finalResultId, Pass.FinalRescale);
buffer.ReleaseTemporaryRT(finalResultId);
}
buffer.ReleaseTemporaryRT(colorGradingLUTId);
}

通过这些修改,HDR颜色也可以正确的插值了,下面是重新缩放LDR颜色时缩放比例为0.5和2的效果:

loading

loading

颜色分级也不再渲染比例为1时引入不存在的色带。需要注意的是我们仅在启用了后处理时解决了这些问题,没有颜色分级,也假设没有HDR。下面是颜色校正后重新缩放,缩放比例为0.5和2时的效果:

loading

loading

16.2.3 双三次采样(Bicubic Sampling)

1. 降低渲染缩放比例时图像会变成块状,于是我们在CameraBufferSettings脚本中添加一个切换开关,对Bloom使用双三次上采样来提高质量,并且在重新缩放最终渲染目标时也可以这么做。

public bool bicubicRescaling;

loading

2. 在PostFXStackPasses.hlsl中添加一个bool字段用于追踪使用的是双三次采样还是常规采样,并定义一个FinalPassFragmentRescale方法进行处理。

bool _CopyBicubic;
float4 FinalPassFragmentRescale (Varyings input) : SV_TARGET
{
if (_CopyBicubic)
{
return GetSourceBicubic(input.screenUV);
}
else
{
return GetSource(input.screenUV);
}
}

3. 我们修改Final Rescale Pass的片元函数,使用FinalPassFragmentRescale这个新方法。

#pragma fragment FinalPassFragmentRescale

4. 在PostFXStack脚本中声明该属性的着色器标识ID,并定义一个bool值来追踪是否使用双三次重新缩放。

int copyBicubicId = Shader.PropertyToID("_CopyBicubic");
bool bicubicRescaling;
public void Setup(..., bool bicubicRescaling)
{
...
this.bicubicRescaling = bicubicRescaling;
...
}

5. 在CameraRenderer.Render方法中传递该参数。

 postFXStack.Setup(context, camera, bufferSize,postFXSettings, useHDR, colorLUTResolution,cameraSettings.finalBlendMode,bufferSettings.bicubicRescaling);

6. 最后在PostFXStack.DoColorGradingAndToneMapping方法中将该字段传递到GPU。

buffer.SetGlobalFloat(copyBicubicId, bicubicRescaling ? 1f : 0f);
DrawFinal(finalResultId, Pass.FinalRescale);

下面是缩放比例为0.25时双线性和双三次重新缩放的效果对比图:

loading

loading

7. 双三次重新缩放在放大缩放比例时可以提高质量,但在缩小比例时差异不明显,它对于缩放比例为2时没有作用,因为每个最终像素都是4个像素的平均值,与双线性插值完全一样。因此我们在CameraBufferSettings脚本中定义一个BicubicRescalingMode枚举,根据枚举选项设置双三次重新缩放的模式,分别是关闭、仅向上采样以及上下采样都有这三种模式。

public enum BicubicRescalingMode 
{
Off,
UpOnly,
UpAndDown
}
public BicubicRescalingMode bicubicRescaling;

8. 在PostFXStack脚本中修改该字段的定义类型。

CameraBufferSettings.BicubicRescalingMode bicubicRescaling;
public void Setup(..., CameraBufferSettings.BicubicRescalingMode bicubicRescaling)
{
...
}

9. 最后调整DoColorGradingAndToneMapping方法,如果我们是缩小渲染缩放比例,则双三次采样仅用于向上采样或者上下采样都有的模式。

bool bicubicSampling = bicubicRescaling == CameraBufferSettings.BicubicRescalingMode.UpAndDown ||
bicubicRescaling == CameraBufferSettings.BicubicRescalingMode.UpOnly && bufferSize.x < camera.pixelWidth;
buffer.SetGlobalFloat(copyBicubicId, bicubicSampling ? 1f : 0f);

源代码及PDF课件地址:

文件下载
共2条评论发表评论
Bob-6378882021-05-24 13:44:52
超级干
zouxiaoqiang2021-05-14 10:19:33
干货啊,感谢作者